﻿/*
 * shaim
 * http://shaim.net
 * Copyright ©2005-2008, shaim dev team
 * Licensed under the Massachusetts Institute of Technology (MIT) License
 * http://www.opensource.org/licenses/mit-license.php
 * 
 */

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.IO;
using System.Xml;
using System.Xml.XPath;
using csammisrun.shaim.ContactListInternal;
using System.Reflection;
using LibGreen;

namespace csammisrun.shaim
{
    /// <summary>
    /// Defines a fully formed contact list
    /// </summary>
    public class ContactList : INotifyPropertyChanged
    {
        /// <summary>
        /// The master list of contacts
        /// </summary>
        private readonly List<Contact> allContacts = new List<Contact>();
        /// <summary>
        /// The master list of groups
        /// </summary>
        private readonly BindingListCollection<ContactGroup> groups;
        /// <summary>
        /// The master list of virtual groups
        /// </summary>
        private readonly List<VirtualContactGroup> virtualGroups = new List<VirtualContactGroup>();
        /// <summary>
        /// The contact group used by "loose" metacontacts, those with no parent assigned
        /// by a protocol plugin
        /// </summary>
        private readonly ContactGroup unattachedContactGroup;
        /// <summary>
        /// The XmlDocument serializing the contact list
        /// </summary>
        private readonly XmlDocument savedList = new XmlDocument();
        /// <summary>
        /// A reference to the shaim core
        /// </summary>
        private readonly ICore shaimCore;
        /// <summary>
        /// An <see cref="IUIDispatcher"/> for contact status updates
        /// </summary>
        private IUIDispatcher uiDispatcher;
        /// <summary>
        /// Keeps a record of protools currently being unmerged
        /// </summary>
        private readonly List<IProtocolPlugin> unmergingProtocols = new List<IProtocolPlugin>();
        /// <summary>
        /// The file path to which the contact list gets serialized
        /// </summary>
        private string serializationPath = "";

        private static Comparison<MetaContact> DisplayNameAsc = DisplayNameComparitor;
        private static Comparison<MetaContact> DisplayNameDesc = DisplayNameComparitorDesc;
        private static Comparison<MetaContact> StatusAsc = StatusComparitor;
        private static Comparison<MetaContact> StatusDesc = StatusComparitorDesc;
        private static Comparison<ContactGroup> GroupDisplayNameAsc = DisplayNameComparitor;
        private static Comparison<ContactGroup> GroupDisplayNameDesc = DisplayNameComparitorDesc;
        private static Predicate<ContactGroup> ShowPopulatedGroups = ShowOnlyPopulatedGroups;
        private static Predicate<MetaContact> ShowOnlineContacts = ShowOnlyOnlineContacts;
        private static Predicate<MetaContact> ShowOfflineOnly = ShowOnlyOfflineContacts;

        private bool showEmptyGroups = true;
        private VirtualContactGroup offlineGroup;
        private bool showOfflineContacts = true;
        private ContactGroupSorting groupSorting = ContactGroupSorting.Manual;
        private MetaContactSorting metacontactSorting = MetaContactSorting.Manual;

        /// <summary>
        /// The XPath expression for locating a metacontact node by a contact
        /// </summary>
        private const string META_FROM_CONTACT_XPATH = "descendant::MetaContact[{0}]";

        /// <summary>
        /// Initializes a new ContactList with a reference to the shaim core
        /// </summary>
        public ContactList(ICore core)
        {
            shaimCore = core;
            if (core != null && core.UISet != null)
            {
                uiDispatcher = core.UISet.CreateDispatcher();
                groups = new BindingListCollection<ContactGroup>(core.UISet.CreateDispatcher());
            }
            else
            {
                groups = new BindingListCollection<ContactGroup>(null);
            }

            groups.PropertyChanged += GroupsPropertyChanged;

            unattachedContactGroup = new ContactGroup(this, "", core);
        }

        private void GroupsPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            if (e.PropertyName == "Items" && PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs("Groups"));
            }
        }

        /// <summary>
        /// Set the UI dispatcher object on the contact list and its groups
        /// </summary>
        /// <remarks>
        /// This is only necessary after the UI plugin has been instantiated for the first time.
        /// Subsequent ContactGroups will create their own dispatchers.
        /// </remarks>
        internal void SetUIDispatcher()
        {
            if (uiDispatcher == null)
            {
                lock (this)
                {
                    uiDispatcher = shaimCore.UISet.CreateDispatcher();
                    Groups.UIDispatcher = shaimCore.UISet.CreateDispatcher();
                    foreach (ContactGroup group in Groups)
                    {
                        if (group != null)
                        {
                            group.Contacts.UIDispatcher = shaimCore.UISet.CreateDispatcher();
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Gets the list of ContactGroups in this contact list
        /// </summary>
        public BindingListCollection<ContactGroup> Groups
        {
            get { return groups; }
        }

        /// <summary>
        /// Raised when a merge or unmerge operation alters the list
        /// </summary>
        public event EventHandler<MergedListEventArgs> MergedListUpdated;

        #region Sorting and filtering operations
        /// <summary>
        /// Gets or sets a value indicating whether groups with no online metacontacts should be displayed
        /// </summary>
        public bool ShowEmptyGroups
        {
            get { return showEmptyGroups; }
            set
            {
                if (showEmptyGroups != value)
                {
                    lock (this)
                    {
                        showEmptyGroups = value;
                        if (showEmptyGroups)
                        {
                            Groups.RemoveFilter();
                        }
                        else
                        {
                            Groups.Filter(ShowPopulatedGroups);
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Gets or sets a value indicating whether offline metacontacts should be put in their own virtual group
        /// </summary>
        public bool UseOfflineGroup
        {
            get
            {
                return offlineGroup != null;
            }
            set
            {
                if (UseOfflineGroup != value)
                {
                    if (offlineGroup == null)
                    {
                        offlineGroup = CreateVirtualGroup("Offline Contacts", ShowOfflineOnly);
                    }
                    else
                    {
                        Unmerge(offlineGroup);
                        offlineGroup = null;
                    }

                    Groups.ApplyListAlterations();
                }
            }
        }

        /// <summary>
        /// Gets or sets a value indicating whether offline metacontacts should be displayed
        /// </summary>
        public bool ShowOfflineContacts
        {
            get { return showOfflineContacts; }
            set
            {
                if (showOfflineContacts != value)
                {
                    lock (this)
                    {
                        showOfflineContacts = value;
                        foreach (ContactGroup group in Groups)
                        {
                            if (!group.IsVirtual)
                            {
                                if (showOfflineContacts)
                                {
                                    group.Contacts.RemoveFilter();
                                }
                                else
                                {
                                    group.Contacts.Filter(ShowOnlineContacts);
                                }
                            }
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Gets or sets the <see cref="ContactGroupSorting"/> algorithm to apply to the contact list
        /// </summary>
        public ContactGroupSorting GroupSorting
        {
            get { return groupSorting; }
            set
            {
                if (groupSorting != value)
                {
                    lock (this)
                    {
                        groupSorting = value;
                        if (groupSorting == ContactGroupSorting.Alphabetic)
                        {
                            Groups.Sort(GroupDisplayNameAsc);
                        }
                        else
                        {
                            Groups.RemoveSort();
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Gets or sets the <see cref="MetaContactSorting"/> algorithm to apply to the contact list
        /// </summary>
        public MetaContactSorting MetacontactSorting
        {
            get { return metacontactSorting; }
            set
            {
                if (metacontactSorting != value)
                {
                    lock (this)
                    {
                        metacontactSorting = value;
                        foreach (ContactGroup group in Groups)
                        {
                            if (!group.IsVirtual)
                            {
                                switch (metacontactSorting)
                                {
                                    case MetaContactSorting.Alphabetic:
                                        group.Contacts.Sort(DisplayNameAsc);
                                        break;
                                    case MetaContactSorting.Manual:
                                        group.Contacts.RemoveSort();
                                        break;
                                    case MetaContactSorting.Status:
                                        group.Contacts.Sort(StatusAsc);
                                        break;
                                }
                            }
                        }
                    }
                }
            }
        }

        #region Filter callbacks
        /// <summary>
        /// Provides a predicate to display only those contact groups with visible items
        /// </summary>
        private static bool ShowOnlyPopulatedGroups(ContactGroup item)
        {
            bool retval = false;
            if (item.ParentList.ShowOfflineContacts)
            {
                retval = item.Count > 0;
            }
            else
            {
                retval = item.OnlineCount > 0;
            }
            return retval;
        }

        /// <summary>
        /// Provides a predicate to display only those items that are not offline
        /// </summary>
        private static bool ShowOnlyOnlineContacts(MetaContact item)
        {
            return item.Status != ContactStatus.Offline;
        }

        /// <summary>
        /// Provides a predicate to display only those items that are offline
        /// </summary>
        private static bool ShowOnlyOfflineContacts(MetaContact item)
        {
            return item.Status == ContactStatus.Offline;
        }
        #endregion

        #region Comparitor methods
        /// <summary>
        /// Compares two metacontacts by their display name, ascending
        /// </summary>
        private static int DisplayNameComparitor(MetaContact a, MetaContact b)
        {
            return a.DisplayName.CompareTo(b.DisplayName);
        }

        /// <summary>
        /// Compares two metacontacts by their display name, descending
        /// </summary>
        private static int DisplayNameComparitorDesc(MetaContact a, MetaContact b)
        {
            return b.DisplayName.CompareTo(a.DisplayName);
        }

        /// <summary>
        /// Compares two metacontacts by their status ascending, then display name ascending
        /// </summary>
        private static int StatusComparitor(MetaContact a, MetaContact b)
        {
            int retval = a.Status.CompareTo(b.Status);
            if (retval == 0)
            {
                retval = a.DisplayName.CompareTo(b.DisplayName);
            }
            return retval;
        }

        /// <summary>
        /// Compares two metacontacts by their status descending, then display name ascending
        /// </summary>
        private static int StatusComparitorDesc(MetaContact a, MetaContact b)
        {
            int retval = b.Status.CompareTo(a.Status);
            if (retval == 0)
            {
                retval = a.DisplayName.CompareTo(b.DisplayName);
            }
            return retval;
        }

        /// <summary>
        /// Compares two groups by their display name, ascending
        /// </summary>
        private static int DisplayNameComparitor(ContactGroup a, ContactGroup b)
        {
            return a.DisplayName.CompareTo(b.DisplayName);
        }

        /// <summary>
        /// Compares two groups by their display name, descending
        /// </summary>
        private static int DisplayNameComparitorDesc(ContactGroup a, ContactGroup b)
        {
            return b.DisplayName.CompareTo(a.DisplayName);
        }
        #endregion
        #endregion

        #region Merging and unmerging operations
        /// <summary>
        /// Announce that a protocol plugin has finished merging its contacts into the list
        /// </summary>
		//public void AnnounceMergedList(IProtocolPlugin protocol)
		//{
		//    // Ensure that the contact list is being serialized when it's populated
		//    Save();

		//    if (MergedListUpdated != null)
		//    {
		//        MergedListUpdated(this, new MergedListEventArgs(protocol, false));
		//    }
		//}

        /// <summary>
        /// Unmerges the items owned by a IProtocolPlugin from the list
        /// </summary>
		//public void Unmerge(IProtocolPlugin protocol)
		//{
		//    if (unmergingProtocols.Contains(protocol))
		//    {
		//        return;
		//    }

		//    unmergingProtocols.Add(protocol);

		//    lock (this)
		//    {
		//        // Save the list in case something good gets unmerged
		//        Save();

		//        bool needs_regeneration = false;
		//        for (int i = 0, j = allContacts.Count; i < j; i++)
		//        {
		//            if (allContacts[i].Protocol == protocol)
		//            {
		//                Unmerge(allContacts[i], true, false);
		//                allContacts.RemoveAt(i);
		//                i--; j--;
		//                needs_regeneration = true;
		//            }
		//        }

		//        if (needs_regeneration)
		//        {
		//            // Update the binding on each dirty group
		//            foreach (ContactGroup group in Groups)
		//            {
		//                group.Contacts.Refresh();
		//            }

		//            foreach (VirtualContactGroup vgroup in virtualGroups)
		//            {
		//                vgroup.Contacts.Refresh();
		//            }

		//            // Let non-binding listeners know about a list update
		//            if (MergedListUpdated != null)
		//            {
		//                MergedListUpdated(this, new MergedListEventArgs(protocol, true));
		//            }
		//        }
		//    }

		//    unmergingProtocols.Remove(protocol);
		//}

        /// <summary>
        /// Unmerges a single ContactGroup from the full list,
        /// removing any child Contacts and emptied metacontacts
        /// </summary>
        /// <remarks>If this ContactList has been merged with another full
        /// ContactList such that groups from the two lists overlap, this method
        /// may not actually remove the ContactGroup from the collection.</remarks>
        public void Unmerge(ContactGroup group)
        {
            Unmerge(group, false);
        }

        /// <summary>
        /// Unmerges a single ContactGroup from the full list,
        /// removing any child Contacts and emptied metacontacts
        /// </summary>
        /// <remarks>If this ContactList has been merged with another full
        /// ContactList such that groups from the two lists overlap, this method
        /// may not actually remove the ContactGroup from the collection.</remarks>
        private void Unmerge(ContactGroup group, bool suppressrefresh)
        {
            lock (this)
            {
                if (group.IsVirtual)
                {
                    VirtualContactGroup vcg = group as VirtualContactGroup;
                    virtualGroups.Remove(vcg);
                    Groups.Remove(group);
                }
                else
                {
                    int i, j;

                    // Preload the target lists so they don't resize.  This preloading is optimal for one
                    // protocol signed in at a time.  More than one will result in a bit of wasted memory,
                    // but still avoids expensive resizing operations.
                    List<Contact> targets = new List<Contact>(group.Contacts.Count);
                    List<MetaContact> metatargets = new List<MetaContact>(group.Contacts.Count);

                    // Enumerate the Contact objects owned by this group
                    for (i = 0, j = group.Contacts.Count; i < j; i++)
                    {
                        for (int x = 0, y = group.Contacts[i].Contacts.Count; x < y; x++)
                        {
                            targets.Add(group.Contacts[i].Contacts[x]);
                        }
                    }

                    for (i = 0, j = metatargets.Count; i < j; i++)
                    {
                        metatargets[i].Parent.Contacts.RaiseListChangedEvents = false;
                        metatargets[i].Parent.Contacts.Remove(metatargets[i]);
                        foreach (VirtualContactGroup vgroup in virtualGroups)
                        {
                            vgroup.Contacts.Remove(metatargets[i]);
                        }
                    }

                    // Remove the group whole-sale if it's now devoid of metacontacts
                    if (group.Contacts.Count == 0)
                    {
                        Groups.Remove(group);
                    }
                }

                if (!suppressrefresh)
                {
                    if (MergedListUpdated != null)
                    {
                        MergedListUpdated(this, new MergedListEventArgs(null, true));
                    }
                }
            }
        }

        /// <summary>
        /// Unmerges a single Contact from the full list
        /// </summary>
        public void Unmerge(Contact contact)
        {
            Unmerge(contact, false, true);
        }

        /// <summary>
        /// Unmerges a single Contact from the full list
        /// </summary>
        /// <param name="contact">The contact to unmerge</param>
        /// <param name="suppressrefresh">A value indicating whether the item removal should trigger
        /// event chains in its containing <see cref="MetaContact"/> and <see cref="ContactGroup"/></param>
        /// <param name="removeitem">A value indicating whether or not the item should be removed from the master internal list</param>
        private void Unmerge(Contact contact, bool suppressrefresh, bool removeitem)
        {
            lock (this)
            {
				//// Remove the metacontact from its group if it's empty
				//if (contact.Parent.Contacts.Count == 0)
				//{
				//    BindingListCollection<MetaContact> list = contact.Parent.Parent.Contacts;
				//    if (suppressrefresh)
				//    {
				//        list.RaiseListChangedEvents = false;
				//        list.Remove(contact.Parent);
				//        list.NeedsBindingRefresh = true;
				//        list.RaiseListChangedEvents = true;
				//    }
				//    else
				//    {
				//        list.Remove(contact.Parent);
				//    }

				//    // Remove the metacontact from any virtual groups
				//    foreach (VirtualContactGroup vgroup in virtualGroups)
				//    {
				//        list = vgroup.Contacts;
				//        if (suppressrefresh)
				//        {
				//            list.RaiseListChangedEvents = false;
				//            list.Remove(contact.Parent);
				//            list.NeedsBindingRefresh = true;
				//            list.RaiseListChangedEvents = true;
				//        }
				//        else
				//        {
				//            list.Remove(contact.Parent);
				//        }
				//    }
				//}

				//// Remove the group from the list if it's empty
				//if (contact.Parent.Parent.Contacts.Count == 0)
				//{
				//    Groups.Remove(contact.Parent.Parent);
				//}

                // Remove the item from the master list, if the caller isn't going to do it
                if (removeitem)
                {
                    allContacts.Remove(contact);
                }
            }
        }
        #endregion

        #region Factories and accessors
        /// <summary>
        /// Updates a <see cref="Contact"/>'s status
        /// </summary>
        /// <param name="contact">The <see cref="Contact"/> to update</param>
        /// <param name="newStatus">The new status to assign to the contact</param>
        /// <remarks>This method ensures that the contact's stauts is updated on the UI thread to prevent
        /// thread access violations.  If no UI is currently active, the status is updated on the calling thread.</remarks>
        public void UpdateContactStatus(Contact contact, ContactStatus newStatus)
        {
            if (uiDispatcher == null || uiDispatcher.CheckAccess())
            {
                contact.Status = newStatus;
            }
            else if (uiDispatcher != null)
            {
                uiDispatcher.Dispatch(new UpdateContactStatusHandler(UpdateContactStatusCore),
                    new UpdateContactStatusData(contact, newStatus));
            }
        }

        /// <summary>
        /// Updates a <see cref="Contact"/> property
        /// </summary>
        /// <param name="contact">The <see cref="Contact"/> to update</param>
        /// <param name="propertyName">The name of the property to update</param>
        /// <param name="newValue">The new value to assign to the property</param>
        /// <remarks>This method ensures that the contact's stauts is updated on the UI thread to prevent
        /// thread access violations.  If no UI is currently active, the status is updated on the calling thread.</remarks>
        public void UpdateContactProperty(Contact contact, string propertyName, object newValue)
        {
            PropertyInfo property = contact.GetType().GetProperty(propertyName);
            if (property == null || !property.IsDefined(typeof(SettableContactPropertyAttribute), false))
            {
                throw new ArgumentException(String.Format("{0} is not settable by this method", propertyName), "propertyName");
            }

            if (uiDispatcher == null || uiDispatcher.CheckAccess())
            {
                UpdateContactPropertyCore(new UpdateContactPropertyData(contact, property, newValue));
            }
            else
            {
                uiDispatcher.Dispatch(new UpdateContactPropertyHandler(UpdateContactPropertyCore),
                    new UpdateContactPropertyData(contact, property, newValue));
            }
        }

        private struct UpdateContactPropertyData
        {
            public readonly Contact contact;
            public readonly PropertyInfo Property;
            public readonly object NewValue;

            /// <summary>
            /// Initializes a new UpdateContactPropertyData
            /// </summary>
            public UpdateContactPropertyData(Contact contact, PropertyInfo property, object newValue)
            {
                this.contact = contact;
                Property = property;
                NewValue = newValue;
            }
        }

        /// <summary>
        /// Provides a delegate for dispatching contact property updates
        /// </summary>
        private delegate void UpdateContactPropertyHandler(UpdateContactPropertyData data);

        /// <summary>
        /// Handles updating a contact's property when invoked through the UI dispatcher
        /// </summary>
        private void UpdateContactPropertyCore(UpdateContactPropertyData data)
        {
            data.Property.SetValue(data.contact, data.NewValue, null);
        }

        private struct UpdateContactStatusData
        {
            public readonly Contact Contact;
            public readonly ContactStatus Status;

            /// <summary>
            /// Initializes a new UpdateContactStatusData
            /// </summary>
            public UpdateContactStatusData(Contact contact, ContactStatus status)
            {
                Contact = contact;
                Status = status;
            }
        }

        /// <summary>
        /// Provides a delegate for dispatching contact status updates
        /// </summary>
        private delegate void UpdateContactStatusHandler(UpdateContactStatusData data);

        /// <summary>
        /// Handles updating a contact's status when invoked through the UI dispatcher
        /// </summary>
        private void UpdateContactStatusCore(UpdateContactStatusData data)
        {
            data.Contact.Status = data.Status;
        }

        /// <summary>
        /// Registers a new Contact into the list
        /// </summary>
        /// <param name="source">The IProtocolPlugin creating the contact</param>
        /// <param name="contactName">The protocol-specific name of the contact</param>
        /// <returns>A fully-connected Contact</returns>
        /// <remarks>
        /// This overload registers a transient contact without an attached group and with its status
        /// set to Unknown
        /// </remarks>
		//public Contact RegisterContact(IProtocolPlugin source, string contactName)
		//{
		//    Contact retval = RegisterContact(source, contactName, 0, unattachedContactGroup);
		//    retval.Status = ContactStatus.Unknown;
		//    return retval;
		//}

        /// <summary>
        /// Registers a new Contact into the list
        /// </summary>
        /// <param name="source">The IProtocolPlugin creating the contact</param>
        /// <param name="contactName">The protocol-specific name of the contact</param>
        /// <param name="contactOrder">The preferred order of the contact within its group</param>
        /// <param name="contactGroup">The <see cref="ContactGroup"/> in which to insert the contact</param>
        /// <returns>A fully-connected Contact</returns>
        /// <remarks>
        /// This is a one-stop shop for connecting up a Contact, its MetaContact, and the containing ContactGroup
        /// </remarks>
		//public Contact RegisterContact(IProtocolPlugin source, string contactName, int contactOrder, ContactGroup contactGroup)
		//{
		//    // Ensure that the contact is being added with a valid preferred order
		//    if (contactOrder < 0)
		//    {
		//        if (Core.GetCoreInstance() != null)
		//        {
		//            throw new ArgumentOutOfRangeException("contactOrder",
		//              String.Format(CultureInfo.CurrentUICulture, Core.FindString("ContactListContactNoOrder"), contactName));
		//        }
		//        else
		//        {
		//            throw new ArgumentOutOfRangeException("contactOrder");
		//        }
		//    }

		//    lock (this)
		//    {
		//        // Create and add the new contact
		//        Contact contact = new Contact(source, source.UsernameFormatter(contactName));
		//        allContacts.Add(contact);
		//        // Get a metacontact for the new contact
		//        MetaContact metacontact = GetMetaContact(contact);
		//        // If this is the first contact in the metacontact, save the preferred order within the group
		//        if (metacontact.PreferredOrder == -1)
		//        {
		//            metacontact.PreferredOrder = contactOrder;
		//        }

		//        // Add the metacontact to its group
		//        // If the requested contact order is over the current
		//        // number of groups, just stick it at the end and it'll
		//        // work itself out.  No, seriously.
		//        if (contactOrder > contactGroup.Contacts.Count)
		//        {
		//            contactOrder = contactGroup.Contacts.Count;
		//        }

		//        contactGroup.Contacts.Insert(contactOrder, metacontact);
		//        // Attach the group to the metacontact and contact
		//        contact.Group = contactGroup;
		//        metacontact.Parent = contactGroup;

		//        return contact;
		//    }
		//}

        /// <summary>
        /// Registers a new Contact into the list
        /// </summary>
        /// <param name="source">The IProtocolPlugin creating the contact</param>
        /// <param name="contactName">The protocol-specific name of the contact</param>
        /// <param name="groupName">The name of the group to which to add the contact</param>
        /// <param name="contactOrder">The preferred order of the contact within its group</param>
        /// <param name="groupOrder">The preferred order of the group in the list</param>
        /// <returns>A fully-connected Contact</returns>
        /// <remarks>
        /// This is a one-stop shop for connecting up a Contact, its MetaContact, and the containing ContactGroup.
        /// Side-effects of this method may include ContactGroup creation and Disco Fever
        /// </remarks>
		//public Contact RegisterContact(IProtocolPlugin source, string contactName, int contactOrder, string groupName, int groupOrder)
		//{
		//    lock (this)
		//    {
		//        // Connect the appropriate group to its children
		//        ContactGroup group;
		//        if (String.IsNullOrEmpty(groupName))
		//        {
		//            group = unattachedContactGroup;
		//        }
		//        else
		//        {
		//            group = GetContactGroup(groupName, groupOrder);
		//        }

		//        return RegisterContact(source, contactName, contactOrder, group);
		//    }
		//}

        /// <summary>
        /// Insert a new group into the contact list at the indicated position
        /// </summary>
        public ContactGroup InsertGroup(string name, int preferredposition)
        {
            lock (this)
            {
                return GetContactGroup(name, preferredposition);
            }
        }

        /// <summary>
        /// Creates a virtual group with the specified name and filter
        /// </summary>
        /// <returns>A virtualized <see cref="ContactGroup"/></returns>
        public VirtualContactGroup CreateVirtualGroup(string name, Predicate<MetaContact> filter)
        {
            lock (this)
            {
                VirtualContactGroup retval = GetContactGroup(name, Int32.MaxValue, true) as VirtualContactGroup;
                retval.Filter = filter;
                retval.Contacts.Sort(DisplayNameAsc);
                return retval;
            }
        }

        /// <summary>
        /// Returns a list of Contact objects registered with the list that match the given key
        /// </summary>
        /// <remarks>This method is perhaps one of the most frequently called in the entire contact
        /// list object.  It needs to be fast and light, and one might wonder why the return list
        /// isn't constructed with a capacity greater than the default to avoid resizing (as is common
        /// elsewhere).  Reason being, I have not once seen an instance in which more than four identical
        /// contacts will be returned by this method; in fact, it used to just return one until I found
        /// some AIM contact lists with two.</remarks>
		//public List<Contact> GetFullContacts(IProtocolPlugin source, string key)
		//{
		//    lock (this)
		//    {
		//        key = source.UsernameFormatter(key);

		//        Contact sc;
		//        List<Contact> retval = new List<Contact>();

		//        for (int i = 0, j = allContacts.Count; i < j; i++)
		//        {
		//            sc = allContacts[i];
		//            if (sc.Protocol == source)
		//            {
		//                if (source.UsernameFormatter(sc.Name) == key)
		//                {
		//                    retval.Add(sc);
		//                }
		//            }
		//        }
		//        return retval;
		//    }
		//}

		///// <summary>
		///// Returns a MetaContact for a Contact
		///// </summary>
		///// <remarks>This looks like it should be a method on Contact, but eventually there will be
		///// core-related aliasing stuff that needs to be seperated</remarks>
		//private MetaContact GetMetaContact(Contact contact)
		//{
		//    MetaContact retval;
		//    if (contact.Parent == null)
		//    {
		//        XmlNode metaContactNode = FindMetaNodeByContact(contact);
		//        if (metaContactNode != null)
		//        {
		//            retval = new MetaContact(uiDispatcher, metaContactNode);
		//        }
		//        else
		//        {
		//            retval = new MetaContact(uiDispatcher);
		//        }
		//        contact.Parent = retval;
		//        retval.Contacts.Add(contact);
		//        // Add the metacontact to each registered virtual group
		//        foreach (VirtualContactGroup group in virtualGroups)
		//        {
		//            group.Contacts.Add(retval);
		//        }
		//    }
		//    else
		//    {
		//        retval = contact.Parent;
		//        if (!retval.Contacts.Contains(contact))
		//        {
		//            retval.Contacts.Add(contact);
		//        }
		//    }

		//    retval.RequestDisplayNameUpdate(contact.Protocol, contact.DisplayName);
		//    return retval;
		//}

        /// <summary>
        /// Returns a ContactGroup corresponding to the given name in the given position
        /// </summary>
        private ContactGroup GetContactGroup(string name, int order)
        {
            return GetContactGroup(name, order, false);
        }

        /// <summary>
        /// Returns a ContactGroup corresponding to the given name in the given position
        /// </summary>
        private ContactGroup GetContactGroup(string name, int order, bool isvirtual)
        {
            lock (this)
            {
                ContactGroup retval;
                // See if the contact group is already in the list (might not be visible)
                for (int i = 0, j = Groups.UnderlyingList.Count; i < j; i++)
                {
                    ContactGroup test = Groups.UnderlyingList[i];
                    if (test != null && test.IsVirtual == isvirtual && test.DisplayName == name)
                    {
                        return test;
                    }
                }

                // Ensure that a valid order has been specified
                if (order < 0)
                {
                    if (Core.GetCoreInstance() != null)
                    {
                        throw new ArgumentOutOfRangeException("order",
                          String.Format(CultureInfo.CurrentUICulture, Core.FindString("ContactListGroupNoOrder"), name));
                    }
                    else
                    {
                        throw new ArgumentOutOfRangeException("order");
                    }
                }

                if (isvirtual)
                {
                    VirtualContactGroup vcg = new VirtualContactGroup(this, name, shaimCore);
                    //PopulateVirtualGroup(vcg);
                    //virtualGroups.Add(vcg);
                    retval = vcg;
                }
                else
                {
                    retval = new ContactGroup(this, name, shaimCore);
                }
                retval.PreferredOrder = order;
                AttachListPropertiesToGroup(retval);

                if (order > Groups.Count)
                {
                    order = Groups.Count;
                }
                Groups.Insert(order, retval);

                try
                {
                    // Check to see if it was previously serialized, and load information from that
                    XmlNode groupnode = savedList.SelectSingleNode("/Contactlist/" + retval.XPath);
                    if (groupnode != null)
                    {
                        retval.Collapsed = (groupnode.Attributes["Collapsed"].Value == "Yes");
                    }
                }
                catch (XPathException)
                {
                }

                return retval;
            }
        }

        /// <summary>
        /// Populates a virtual contact group with the existing full contact list
        /// </summary>
		//private void PopulateVirtualGroup(VirtualContactGroup virtualContactGroup)
		//{
		//    lock (this)
		//    {
		//        foreach (Contact contact in allContacts)
		//        {
		//            if (!virtualContactGroup.Contacts.Contains(contact.Parent))
		//            {
		//                virtualContactGroup.Contacts.Add(contact.Parent);
		//            }
		//        }
		//    }
		//}

        /// <summary>
        /// Attaches sorting, filtering, and UI dispatch properties to a new <see cref="ContactGroup" />
        /// </summary>
        private void AttachListPropertiesToGroup(ContactGroup group)
        {
            if (shaimCore != null)
            {
                group.Contacts.UIDispatcher = shaimCore.UISet.CreateDispatcher();
                // Don't re-filter or re-sort virtual groups
                if (!group.IsVirtual)
                {
                    switch (metacontactSorting)
                    {
                        case MetaContactSorting.Alphabetic:
                            group.Contacts.Sort(DisplayNameAsc);
                            break;
                        case MetaContactSorting.Manual:
                            group.Contacts.RemoveSort();
                            break;
                        case MetaContactSorting.Status:
                            group.Contacts.Sort(StatusAsc);
                            break;
                    }

                    if (showOfflineContacts)
                    {
                        group.Contacts.RemoveFilter();
                    }
                    else
                    {
                        group.Contacts.Filter(ShowOnlineContacts);
                    }
                }
            }
        }
        #endregion

        #region Serialization
        /// <summary>
        /// Gets or sets a path to which to save the contact list after merge operations
        /// </summary>
        internal string SerializationPath
        {
            get { return serializationPath; }
            set
            {
                serializationPath = value;

                if (File.Exists(serializationPath))
                {
                    try
                    {
                        savedList.Load(serializationPath);
                    }
                    catch (XmlException)
                    {
                        shaimCore.LogMessage(null, ShaimLogLevel.Warning,
                          Core.FindString("contactListSerializationPathError"), serializationPath);
                    }
                }
            }
        }

        /// <summary>
        /// Returns a metacontact XML node by a child Contact
        /// </summary>
		//private XmlNode FindMetaNodeByContact(Contact sc)
		//{
		//    string xpath = String.Format(CultureInfo.InvariantCulture, META_FROM_CONTACT_XPATH, sc.XPath);

		//    try
		//    {
		//        return savedList.SelectSingleNode(xpath);
		//    }
		//    catch (XPathException)
		//    {
		//    }

		//    return null;
		//}

        /// <summary>
        /// Serializes the contact list to an XML file specified by SerializationPath
        /// </summary>
		//internal void Save()
		//{
		//    if (String.IsNullOrEmpty(serializationPath))
		//    {
		//        return;
		//    }

		//    XmlNode root = savedList.SelectSingleNode("/Contactlist");
		//    if (root == null)
		//    {
		//        root = savedList.CreateNode(XmlNodeType.Element, "Contactlist", savedList.NamespaceURI);
		//        savedList.AppendChild(root);
		//    }

		//    lock (this)
		//    {
		//        foreach (ContactGroup group in Groups.UnderlyingList)
		//        {
		//            if (group == null)
		//            {
		//                continue;
		//            }

		//            XmlNode groupnode = root.SelectSingleNode(group.XPath);
		//            if (groupnode == null)
		//            {
		//                groupnode = savedList.CreateNode(XmlNodeType.Element, "Group", savedList.NamespaceURI);
		//                root.AppendChild(groupnode);
		//            }
		//            group.SetXmlNodeAttributes(groupnode);

		//            // Don't serialize the children of virtual groups
		//            if (group.IsVirtual)
		//            {
		//                continue;
		//            }


		//            foreach (MetaContact metaContact in group.Contacts.UnderlyingList)
		//            {
		//                if (metaContact == null)
		//                {
		//                    continue;
		//                }

		//                XmlNode metanode = groupnode.SelectSingleNode(metaContact.XPath);
		//                if (metanode == null)
		//                {
		//                    metanode = savedList.CreateNode(XmlNodeType.Element, "MetaContact", savedList.NamespaceURI);
		//                    groupnode.AppendChild(metanode);
		//                }
		//                metaContact.SetXmlNodeAttributes(metanode);

		//                foreach (Contact contact in metaContact.Contacts)
		//                {
		//                    XmlNode contactnode = metanode.SelectSingleNode(contact.XPath);
		//                    if (contactnode == null)
		//                    {
		//                        contactnode = savedList.CreateNode(XmlNodeType.Element, "Contact", savedList.NamespaceURI);
		//                        metanode.AppendChild(contactnode);
		//                    }
		//                    contact.SetXmlNodeAttributes(contactnode);
		//                }
		//            }
		//        }
		//    }


		//    try
		//    {
		//        savedList.Save(serializationPath);
		//    }
		//    catch (XmlException xmlex)
		//    {
		//        if (shaimCore != null)
		//        {
		//            shaimCore.LogMessage(null, ShaimLogLevel.Warning, "Could not save core contact list: {0}", xmlex);
		//        }
		//    }
		//}
		#endregion

        /// <summary>
        /// Escape a string so that it can be used as an XML attribute
        /// </summary>
        internal static string EscapeAttribute(string attribute)
        {
            return attribute.Replace("'", "&#39;");
        }

        #region INotifyPropertyChanged Members

        /// <summary>
        /// Raised when a proprety of the contact list has changed
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;

        #endregion
    }

    /// <summary>
    /// Contains data concerning a core contact list merging or unmerging event
    /// </summary>
    public class MergedListEventArgs : EventArgs
    {
        private readonly bool wasListUnmerged;
        private readonly IProtocolPlugin listOwner;

        /// <summary>
        /// Initializes a new MergedListEventArgs
        /// </summary>
        /// <param name="listOwner">The protocol plugin that initiated the list event</param>
        /// <param name="wasListUnmerged">A value indicating whether a protocol-specific list was
        /// merged into or unmerged from the core list</param>
        public MergedListEventArgs(IProtocolPlugin listOwner, bool wasListUnmerged)
        {
            this.listOwner = listOwner;
            this.wasListUnmerged = wasListUnmerged;
        }

        /// <summary>
        /// Gets a value indicating whether a protocol-specific list was merged into or unmerged from
        /// the core list
        /// </summary>
        public bool WasListUnmerged
        {
            get { return wasListUnmerged; }
        }

        /// <summary>
        /// Gets the protocol plugin that owns the list that was merged or unmerged
        /// </summary>
        public IProtocolPlugin ListOwner
        {
            get { return listOwner; }
        }
    }

	/// <summary>
	/// A collection of <see cref="Contact"/>s
	/// </summary>
	public class ContactCollection : IList<Contact>
	{
		private readonly List<Contact> contacts = new List<Contact> ();
		private readonly MetaContact parent;
		/// <summary>
		/// Initializes a new ContactList belonging to the specified metacontact
		/// </summary>
		public ContactCollection (MetaContact parent)
		{
			this.parent = parent;
		}

		#region IList<Contact> Members
		/// <summary>
		/// Determines the index of a specific item in the IList.
		/// </summary>
		/// <param name="item"></param>
		/// <returns></returns>
		public int IndexOf (Contact item)
		{
			return contacts.IndexOf (item);
		}

		/// <summary>
		/// Inserts an item to the IList at the specified index.
		/// </summary>
		/// <param name="index"></param>
		/// <param name="item"></param>
		public void Insert (int index, Contact item)
		{
			contacts.Insert (index, item);
			// Add the status update handler and immediately call it to force a status recalculation
			item.StatusUpdate += parent.ContactStatusUpdate;
			item.PropertyChanged += parent.ContactPropertyUpdate;
			parent.ContactStatusUpdate (item, new StatusUpdateEventArgs (ContactStatus.Offline, item.Status));
		}

		/// <summary>
		/// Removes the IList item at the specified index.
		/// </summary>
		/// <param name="index"></param>
		public void RemoveAt (int index)
		{
			Contact target = contacts[index];
			Remove (target);
		}

		/// <summary>
		/// Gets or sets the element at the specified index.
		/// </summary>
		/// <param name="index"></param>
		/// <returns></returns>
		public Contact this[int index]
		{
			get
			{
				return contacts[index];
			}
			set
			{
				contacts[index] = value;
			}
		}

		#endregion

		#region ICollection<Contact> Members
		/// <summary>
		/// Adds an item to the ICollection.
		/// </summary>
		/// <param name="item"></param>
		public void Add (Contact item)
		{
			contacts.Add (item);
			// Add the status update handler and immediately call it to force a status recalculation
			item.StatusUpdate += parent.ContactStatusUpdate;
			item.PropertyChanged += parent.ContactPropertyUpdate;
			parent.ContactStatusUpdate (item, new StatusUpdateEventArgs (ContactStatus.Offline, item.Status));
		}

		/// <summary>
		/// Removes all items from the ICollection.
		/// </summary>
		public void Clear ()
		{
			while (contacts.Count > 0) {
				Remove (contacts[0]);
			}
		}

		/// <summary>
		/// Determines whether the ICollection contains a specific value.
		/// </summary>
		/// <param name="item"></param>
		/// <returns></returns>
		public bool Contains (Contact item)
		{
			return contacts.Contains (item);
		}

		/// <summary>
		/// Copies the elements of the ICollection to an Array, starting at a particular Array index.
		/// </summary>
		/// <param name="array"></param>
		/// <param name="arrayIndex"></param>
		public void CopyTo (Contact[] array, int arrayIndex)
		{
			contacts.CopyTo (array, arrayIndex);
		}

		/// <summary>
		/// Gets the number of elements contained in the ICollection.
		/// </summary>
		public int Count
		{
			get { return contacts.Count; }
		}

		/// <summary>
		/// Gets a value indicating whether the ICollection is read-only.
		/// </summary>
		public bool IsReadOnly
		{
			get { return false; }
		}

		/// <summary>
		/// Removes the first occurrence of a specific object from the ICollection.
		/// </summary>
		/// <param name="item"></param>
		/// <returns></returns>
		public bool Remove (Contact item)
		{
			bool retval = contacts.Remove (item);
			if (retval) {
				// Remove the event handler from the contact and force a recalc on the parent
				item.StatusUpdate -= parent.ContactStatusUpdate;
				item.PropertyChanged -= parent.ContactPropertyUpdate;
				parent.ContactStatusUpdate (item, new StatusUpdateEventArgs (ContactStatus.Offline, item.Status));
			}
			return retval;
		}

		/// <summary>
		/// Remove a Contact from the list without notifying its parent and triggering an event series
		/// </summary>
		/// <remarks>
		/// No recalculation is done on the metacontact that owns the collection, so this is only to be used
		/// in an unmerge.  The MergedListUpdated event on the contact list will cause the correct event series.
		/// </remarks>
		internal bool RemoveFast (Contact contact)
		{
			bool retval = contacts.Remove (contact);
			contact.StatusUpdate -= parent.ContactStatusUpdate;
			contact.PropertyChanged -= parent.ContactPropertyUpdate;
			return retval;
		}

		#endregion

		#region IEnumerable<Contact> Members
		/// <summary>
		/// Returns an enumerator that iterates through the collection.
		/// </summary>
		/// <returns></returns>
		public IEnumerator<Contact> GetEnumerator ()
		{
			return contacts.GetEnumerator ();
		}

		#endregion

		#region IEnumerable Members
		/// <summary>
		/// Returns an enumerator that iterates through a collection.
		/// </summary>
		/// <returns></returns>
		System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator ()
		{
			return contacts.GetEnumerator ();
		}

		#endregion
	}

}
